﻿using System;
using System.Collections.Generic;
using System.Transactions;

namespace PDO.DataPersistence.ServiceProvider
{
	/// <summary>
	/// Map Transaction IDs from the client to server-local Transactions.
	/// Sets Transaction.Current.
	/// EntLibDataBlock\TransactionScopeConnections maps Transaction.Current to database connections.
	/// </summary>
	/// <remarks>
	/// System.Transaction exists per thread.
	/// Database transactions exist per database connection.
	/// Supports nested transactions.
	/// </remarks>
	class TransactionTracker
	{
		private enum TransactionWrappedStatus
		{
			Activated,
			Committed,
			RolledBack
		}
	
		/// <summary>
		/// Wrapper to maintain a transaction reference count for nested transactions.
		/// </summary>
		private class TransactionWrapper : IDisposable
		{
			public int count = 0;
			public TransactionWrappedStatus status = TransactionWrappedStatus.Activated;
			public CommittableTransaction transaction = null;

			public TransactionWrapper(string transactionid, int timeoutseconds)
			{
				count = 0;
				if (Transaction.Current == null || Transaction.Current.TransactionInformation.LocalIdentifier != transactionid)
				{
					transaction = new CommittableTransaction(new TimeSpan(0, 0, timeoutseconds));
					status = TransactionWrappedStatus.Activated;
				}
				else
				{
					transaction = Transaction.Current as CommittableTransaction;
					switch (transaction.TransactionInformation.Status)
					{
						case TransactionStatus.Active: status = TransactionWrappedStatus.Activated; break;
						case TransactionStatus.Committed: status = TransactionWrappedStatus.Committed; break;
						case TransactionStatus.Aborted: status = TransactionWrappedStatus.RolledBack; break;
						default: status = TransactionWrappedStatus.RolledBack; break;
					}
				}
			}

			public void Dispose()
			{
				if (transaction != null)
					transaction.Dispose();
			}
		}


		private int _defaultTimeoutSeconds = 30;
		private Dictionary<string, TransactionWrapper> _tranDict;


		public TransactionTracker()
		{
			_tranDict = new Dictionary<string, TransactionWrapper>();
		}

		public int DefaultTimeoutSeconds
		{
			get { return _defaultTimeoutSeconds; }
			set 
			{ 
				#if TRANSACTION_TEST
				Log("Transaction timeout = {0}", value);
				#endif
				_defaultTimeoutSeconds = value; 
			}
		}

		/// <summary>
		/// Add a transaction to the dictionary. Equivalent to Begin Transaction.
		/// </summary>
		/// <param name="transactionid">An id assigned by the client.</param>
		/// <param name="timeoutseconds">Transaction timeout in seconds.</param>
		public CommittableTransaction Add(string transactionid, int timeoutseconds)
		{
			if (string.IsNullOrEmpty(transactionid))
				throw new ArgumentException("Transaction ID required");

			TransactionWrapper tr;
			lock (_tranDict)
			{
				if (!_tranDict.TryGetValue(transactionid, out tr))
				{
					tr = new TransactionWrapper(transactionid, timeoutseconds);
					
					tr.transaction.TransactionCompleted += new TransactionCompletedEventHandler(TransactionCompleted);

					_tranDict.Add(transactionid, tr);
				}
			}

			#if TRANSACTION_TEST
			Log("TransactionTracker::Add({0}) = {1} {2} {3}", gTrim(transactionid),
				tr.transaction.TransactionInformation.CreationTime.ToString("HH:mm:ss.ff"),
				gTrim(tr.transaction.TransactionInformation.LocalIdentifier),
				tr.transaction.TransactionInformation.Status);
			#endif

			tr.count++;

			//Transaction.Current = tr.transaction; unnecessary	// don't like this

			return tr.transaction;
		}

		/// <summary>
		/// Add a transaction to the dictionary. Equivalent to Begin Transaction.
		/// </summary>
		/// <param name="transactionid">An id assigned by the client.</param>
		public CommittableTransaction Add(string transactionid)
		{
			return Add(transactionid, _defaultTimeoutSeconds);
		}

		/// <summary>
		/// Locate a transaction by id.
		/// </summary>
		/// <returns>The transaction or null if not found.</returns>
		public CommittableTransaction Find(string transactionid)
		{
			TransactionWrapper tr = _find(transactionid);
			if (tr != null)
				return tr.transaction;
			return null;
		}

		/// <summary>
		/// Commit the specified transaction.
		/// </summary>
		public void Commit(string transactionid)
		{
			if (string.IsNullOrEmpty(transactionid))
				throw new ArgumentException("Transaction ID required");

			#if TRANSACTION_TEST
			Log("TransactionTracker::Commit({0})", gTrim(transactionid));
			#endif
			TransactionWrapper tr = _find(transactionid);
			if (tr == null)
			{
				#if TRANSACTION_TEST
				LogError(string.Format("Commit failed, transaction {0} not found", transactionid));
				#endif
				throw new TransactionException(string.Format("Commit failed, transaction {0} not found", transactionid));
			}
			if (tr.status != TransactionWrappedStatus.Activated)
			{
				#if TRANSACTION_TEST
				LogError(string.Format("Commit failed, transaction {0} already {2}", transactionid, tr.status.ToString()));
				#endif
				throw new TransactionAbortedException(string.Format("Commit failed, transaction {0} already {2}", transactionid, tr.status.ToString()));
			}

			if (--tr.count == 0)
			{
				#if TRANSACTION_TEST
				Log("TransactionTracker::Commit({0}) - Actual Commit", gTrim(transactionid));
				#endif
				Transaction.Current = tr.transaction;	// don't like this
				tr.status = TransactionWrappedStatus.Committed;
				tr.transaction.Commit();
			}
		}

		/// <summary>
		/// Roll back the specified transaction.
		/// </summary>
		public void Rollback(string transactionid)
		{
			if (string.IsNullOrEmpty(transactionid))
				throw new ArgumentException("Transaction ID required");

			#if TRANSACTION_TEST
			LogWarning("TransactionTracker::Rollback({0})", gTrim(transactionid));
			#endif
			TransactionWrapper tr = _find(transactionid);
			if (tr != null)
			{
				if (tr.status != TransactionWrappedStatus.Activated)
				{
					#if TRANSACTION_TEST
					LogError(string.Format("Rollback failed, transaction {0} already {2}", transactionid, tr.status.ToString()));
					#endif
					throw new TransactionAbortedException(string.Format("Rollback failed, transaction {0} already {2}", transactionid, tr.status.ToString()));
				}

				if (--tr.count == 0)
				{
					#if TRANSACTION_TEST
					LogWarning("TransactionTracker::Rollback({0}) - Actual Rollback", gTrim(transactionid));
					#endif
					Transaction.Current = tr.transaction;	// don't like this
					tr.status = TransactionWrappedStatus.RolledBack;
					tr.transaction.Rollback();
				}
			}
		}

		/// <summary>
		/// Called when the system is done with a transaction. Take it out of our list.
		/// </summary>
		protected void TransactionCompleted(object sender, TransactionEventArgs e)
		{
			if (Transaction.Current == e.Transaction)
				Transaction.Current = null;

			lock (_tranDict)
			{
				foreach (KeyValuePair<string, TransactionWrapper> kvp in _tranDict)
				{
					if (kvp.Value.transaction == e.Transaction)
					{
						#if TRANSACTION_TEST
						string msg = string.Format("TransactionTracker::TransactionCompleted {0} : Transaction={1} {2} {3}",
							kvp.Value.status,
							e.Transaction.TransactionInformation.CreationTime.ToString("HH:mm:ss.ff"),
							gTrim(e.Transaction.TransactionInformation.LocalIdentifier),
							e.Transaction.TransactionInformation.Status);

						if ( ((e.Transaction.TransactionInformation.Status == TransactionStatus.Committed)
								&& (kvp.Value.status != TransactionWrappedStatus.Committed))
						  || ((e.Transaction.TransactionInformation.Status == TransactionStatus.Aborted)
								&& (kvp.Value.status != TransactionWrappedStatus.RolledBack)) )
						{
							LogError(msg);
						}
						else
						{
							Log(msg);
						}
						#endif

						_tranDict.Remove(kvp.Key);
						break;
					}
				}
			}
		}

		/// <summary>
		/// Look up a transaction in our list.
		/// </summary>
		/// <returns>The transaction matching the id, or null if not found.</returns>
		private TransactionWrapper _find(string transactionid)
		{
			if (string.IsNullOrEmpty(transactionid))
				return null;

			TransactionWrapper tr;
			try
			{
				lock (_tranDict)
				{
					tr = _tranDict[transactionid];
				}
			}
			catch
			{
				tr = null;
			}

			return tr;
		}

		#region Logging

		#if TRANSACTION_TEST

		// transaction id guids are xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:yy, shorten to something readable, the yy is most important
		private string gTrim(string guid)
		{
			if (guid.Length < 6)
				return guid;
			int x = guid.LastIndexOf(":");
			if (x >= 0)
				return guid.Substring(x - 4);
			return guid;
		}

		public object consoleLock = new object();

		private void Log(string msg, params object[] args)
		{
			lock (consoleLock)
			{
				Console.WriteLine(DateTime.Now.ToString("HH:mm:ss.ff") + " " + msg, args);
			}
		}

		private void LogError(string msg, params object[] args)
		{
			lock (consoleLock)
			{
				Console.ForegroundColor = ConsoleColor.Red;
				Console.WriteLine(DateTime.Now.ToString("HH:mm:ss.ff") + " *** " + msg, args);
				Console.ResetColor();
			}
		}

		private void LogWarning(string msg, params object[] args)
		{
			lock (consoleLock)
			{
				Console.ForegroundColor = ConsoleColor.DarkYellow;
				Console.WriteLine(DateTime.Now.ToString("HH:mm:ss.ff") + " * " + msg, args);
				Console.ResetColor();
			}
		}

		#endif

		#endregion
	}



}
